Skip to content

OSC 8 Hyperlink Support - #207

Open
nschimme wants to merge 8 commits into
masterfrom
osc8-hyperlinks-17754462110983904247
Open

OSC 8 Hyperlink Support#207
nschimme wants to merge 8 commits into
masterfrom
osc8-hyperlinks-17754462110983904247

Conversation

@nschimme

@nschimme nschimme commented Apr 15, 2026

Copy link
Copy Markdown
Owner

This PR adds full support for OSC 8 hyperlinks to the MMapper integrated client.

Key features:

  1. ANSI/OSC Parsing: The ANSI parser was extended to support OSC 8 sequences (ESC ] 8 ; params ; URI ST). It supports both ESC \ and BEL (\x07) as terminators.
  2. Mudlet Schemes: Support for send: (immediate command execution) and prompt: (input pre-filling) schemes was added, following Mudlet's standard.
  3. Synchronized Underlining: Hyperlinks that share an id parameter (e.g., across line breaks or multiple fragments) are highlighted together when any of them is hovered. This was achieved using a viewport event filter and QTextEdit::ExtraSelection.
  4. Security: file:// URIs are restricted to the local host to prevent security risks associated with remote file execution.
  5. Robustness: Updated the tokenizer and regexes to handle OSC sequences without breaking existing CSI/SGR parsing.

The RawAnsi struct was updated to store URL data, which required removing constexpr from its constructors and some utility functions due to the inclusion of QString. Existing constexpr usages in the codebase were updated to const.


PR created automatically by Jules for task 17754462110983904247 started by @nschimme

Summary by Sourcery

Add OSC 8 hyperlink handling to the ANSI/OSC parser and integrated client, enabling clickable and synchronized hyperlinks in MUD output with appropriate security checks and UI behavior.

New Features:

  • Support OSC 8 escape sequences in the ANSI tokenizer and color parser, including URL and hyperlink ID extraction.
  • Render hyperlinks in the display widget as clickable anchors with scheme-specific behavior for send:, prompt:, file:, and generic URLs.
  • Synchronize underline highlighting across all text fragments that share the same hyperlink ID when hovered.

Enhancements:

  • Extend weak ANSI/OSC detection regexes to recognize OSC sequences without impacting existing CSI/SGR parsing.
  • Propagate hyperlink metadata through RawAnsi and text formatting, replacing constexpr usage as needed for QString support.

Tests:

  • Add unit tests covering OSC 8 parsing, including standard URLs, ID parameters, and hyperlink-closer sequences.

Implemented OSC 8 hyperlink support in the integrated client:
- Extended RawAnsi struct to include url and urlId fields.
- Enhanced ANSI parser to recognize and parse OSC 8 sequences (including BEL and ST terminators).
- Updated DisplayWidget to handle hyperlink rendering via QTextCharFormat anchors.
- Implemented support for Mudlet-compatible URI schemes:
  - send: Executes the command immediately (appends \n).
  - prompt: Pre-fills the input widget with the command.
- Added synchronized hover underlining for fragments sharing the same URL ID using setExtraSelections.
- Integrated security check for file:// URIs to ensure they only open local files.
- Updated AnsiTokenizer to correctly skip OSC sequences.
- Added unit tests in TestGlobal for OSC 8 parsing.
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-ai

sourcery-ai Bot commented Apr 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds OSC 8 hyperlink parsing and rendering to the ANSI pipeline, threads URL/URL-id through RawAnsi into QText formatting, and wires the display widget to handle Mudlet-style hyperlink schemes with synchronized hover underlining and basic file:// security checks, while generalizing the ANSI regex/tokenizer and dropping constexpr from RawAnsi due to QString usage.

Sequence diagram for OSC 8 parsing and hyperlink formatting pipeline

sequenceDiagram
    participant TelnetStream
    participant AnsiTokenizer_Iterator as AnsiTokenizerIterator
    participant AnsiTextUtils
    participant AnsiColorParser
    participant RawAnsiState as RawAnsi
    participant AnsiTextHelper
    participant QTextFormat as QTextCharFormat

    TelnetStream->>AnsiTokenizer_Iterator: next escape sequence
    AnsiTokenizer_Iterator->>AnsiTextUtils: weakAnsiRegex match
    AnsiTokenizer_Iterator->>AnsiTextUtils: parseAnsiColor(before, ansiStr)

    alt OSC8 hyperlink
        AnsiTextUtils->>AnsiTextUtils: isOsc8(ansiStr)
        AnsiTextUtils->>AnsiTextUtils: parseOsc8(next, ansiStr)
        AnsiTextUtils-->>AnsiTokenizer_Iterator: RawAnsi next
    else non hyperlink ANSI color
        AnsiTextUtils->>AnsiTextUtils: isAnsiColor(ansiStr)
        AnsiTextUtils->>AnsiColorParser: for_each(ansiStr)
        AnsiColorParser->>RawAnsiState: update color and style
        AnsiTextUtils-->>AnsiTokenizer_Iterator: RawAnsi next
    end

    AnsiTokenizer_Iterator-->>AnsiTextHelper: ansiStr and RawAnsi currentAnsi
    AnsiTextHelper->>AnsiTextUtils: parseAnsiColor(currentAnsi, ansiStr)
    AnsiTextUtils-->>AnsiTextHelper: optional RawAnsi updated

    alt parsing succeeded
        AnsiTextHelper->>QTextFormat: updateFormat(format, defaults, currentAnsi, updated)
        QTextFormat->>QTextFormat: setAnchor(updated.url is not empty)
        QTextFormat->>QTextFormat: setAnchorHref(updated.url)
        QTextFormat->>QTextFormat: setProperty(URL_ID_PROPERTY, updated.urlId)
        AnsiTextHelper->>RawAnsiState: currentAnsi = updated
    else not ANSI or unsupported
        AnsiTextHelper-->>AnsiTokenizer_Iterator: ignore sequence
    end
Loading

Sequence diagram for hyperlink click handling with Mudlet schemes and security checks

sequenceDiagram
    actor User
    participant DisplayWidget
    participant DisplayWidgetOutputs
    participant ClientWidget
    participant Telnet
    participant StackedInputWidget
    participant QDesktopServices
    participant QHostInfo

    User->>DisplayWidget: Click hyperlink
    DisplayWidget-->>DisplayWidget: emit anchorClicked(QUrl url)
    DisplayWidget->>DisplayWidget: lambda anchorClicked handler

    DisplayWidget->>DisplayWidget: scheme = url.scheme()

    alt scheme send
        DisplayWidget->>DisplayWidgetOutputs: sendUserInput(url.path() + newline)
        DisplayWidgetOutputs->>ClientWidget: virt_sendUserInput(msg)
        ClientWidget->>Telnet: sendToMud(msg)
    else scheme prompt
        DisplayWidget->>DisplayWidgetOutputs: setPrompt(url.path())
        DisplayWidgetOutputs->>ClientWidget: virt_setPrompt(msg)
        ClientWidget->>StackedInputWidget: setPrompt(msg)
        StackedInputWidget->>StackedInputWidget: setPlainText(msg)
        StackedInputWidget->>StackedInputWidget: moveCursor(End)
        StackedInputWidget->>StackedInputWidget: setFocus()
    else scheme file
        DisplayWidget->>DisplayWidget: host = url.host()
        DisplayWidget->>QHostInfo: localHostName()
        QHostInfo-->>DisplayWidget: localName
        alt host empty or localhost or localName
            DisplayWidget->>QDesktopServices: openUrl(url)
        else non local host
            DisplayWidget-->>DisplayWidget: log warning and ignore
        end
    else other schemes
        DisplayWidget->>QDesktopServices: openUrl(url)
    end
Loading

Updated class diagram for ANSI hyperlink and OSC 8 support

classDiagram
    class RawAnsi {
        +AnsiColorVariant fg
        +AnsiColorVariant bg
        +AnsiColorVariant ul
        +QString url
        +QString urlId
        -AnsiStyleFlags m_flags
        -AnsiUnderlineStyleEnum m_underlineStyle
        +RawAnsi()
        +RawAnsi(AnsiStyleFlags flags, AnsiColorVariant fg_, AnsiColorVariant bg_, AnsiColorVariant ul_)
        +bool hasForegroundColor()
        +bool hasBackgroundColor()
        +bool hasUnderlineColor()
        +RawAnsi withForeground(AnsiColorVariant var)
        +RawAnsi withBackground(AnsiColorVariant var)
        +RawAnsi withUnderlineColor(AnsiColorVariant var)
        +RawAnsi withUnderlineStyle(AnsiUnderlineStyleEnum style)
        +RawAnsi withForeground(AnsiColor16Enum newColor)
        +RawAnsi withBackground(AnsiColor16Enum newColor)
        +RawAnsi withUnderlineColor(AnsiColor16Enum newColor)
        +bool hasUnderline()
        +void setUnderline()
        +void clearUnderline()
        +void setUnderlineStyle(AnsiUnderlineStyleEnum style)
        +AnsiStyleFlags getFlags()
        +AnsiUnderlineStyleEnum getUnderlineStyle()
        +void setFlag(AnsiStyleFlagEnum flag)
        +void removeFlag(AnsiStyleFlagEnum flag)
        +bool operator==(RawAnsi rhs)
        +bool operator!=(RawAnsi rhs)
    }

    class AnsiTextHelper {
        +static int URL_ID_PROPERTY
        +QTextEdit &textEdit
        +QTextCursor cursor
        +QTextCharFormat format
        +RawAnsi currentAnsi
        +void displayText(QStringView input_str)
    }

    class DisplayWidgetOutputs {
        +void showMessage(QString msg, int timeout)
        +void windowSizeChanged(int width, int height)
        +void returnFocusToInput()
        +void showPreview(bool visible)
        +void sendUserInput(QString msg)
        +void setPrompt(QString msg)
        #virtual void virt_showMessage(QString msg, int timeout)
        #virtual void virt_windowSizeChanged(int width, int height)
        #virtual void virt_returnFocusToInput()
        #virtual void virt_showPreview(bool visible)
        #virtual void virt_sendUserInput(QString msg)
        #virtual void virt_setPrompt(QString msg)
    }

    class DisplayWidget {
        <<QObject>>
        +QString m_lastUrlId
        +DisplayWidget(QWidget *parent)
        +void slot_displayText(QStringView str)
        +void resizeEvent(QResizeEvent *event)
        +void keyPressEvent(QKeyEvent *event)
        +bool eventFilter(QObject *watched, QEvent *event)
        -void updateHoverUnderline(QString urlId)
        +signals void anchorClicked(QUrl url)
    }

    class StackedInputWidget {
        <<QObject>>
        +void gotMultiLineInput(QString input)
        +void gotPasswordInput(QString input)
        +void setPrompt(QString msg)
        +void setEchoMode(EchoModeEnum echoMode)
        +EchoModeEnum getEchoMode()
    }

    class ClientWidget {
        +void initDisplayWidget()
        +StackedInputWidget &getInput()
        +DisplayWidget &getDisplay()
        +Telnet &getTelnet()
        +StackedInputWidget &getPreview()
    }

    class Telnet {
        +void sendToMud(QString msg)
    }

    RawAnsi <.. AnsiTextHelper : uses
    AnsiTextHelper <.. DisplayWidget : uses
    DisplayWidgetOutputs <.. ClientWidget : implements
    DisplayWidget --> DisplayWidgetOutputs : uses
    ClientWidget --> StackedInputWidget : owns
    ClientWidget --> DisplayWidget : owns
    ClientWidget --> Telnet : owns

    class AnsiTokenizer {
        +class Iterator
    }

    class AnsiTokenizer_Iterator {
        +AnsiTokenizer::Iterator::size_type skip_ansi()
        +AnsiStringToken getCurrent()
    }

    AnsiTokenizer_Iterator --|> AnsiTokenizer

    class AnsiColorParser {
        +void for_each(QStringView ansi) const
    }

    RawAnsi <.. AnsiColorParser : updates

    class AnsiTextUtils {
        +bool isAnsiColor(QStringView ansi)
        +bool isAnsiEraseLine(QStringView ansi)
        +std::optional~RawAnsi~ parseAnsiColor(RawAnsi before, QStringView ansi)
        +bool isOsc8(QStringView ansi)
        +void parseOsc8(RawAnsi next, QStringView ansi)
        +QRegularExpression weakAnsiRegex
    }

    AnsiTextUtils ..> RawAnsi : returns
    AnsiTokenizer ..> AnsiTextUtils : uses
Loading

File-Level Changes

Change Details Files
Extend ANSI detection/parsing to understand OSC 8 escape sequences and map them into RawAnsi URL metadata.
  • Broaden weakAnsiRegex and AnsiTextHelper::ansi_regex to match both CSI and OSC 8 sequences with ESC \ or BEL terminators.
  • Add isOsc8 and parseOsc8 helpers to recognize OSC 8 sequences, extract params/URI, and update RawAnsi.url and RawAnsi.urlId, including handling of hyperlink closing sequences.
  • Invoke parseOsc8 early in parseAnsiColor, returning an updated RawAnsi even for non-color sequences, and add unit tests verifying OSC 8 parsing including id parameter and closer behavior.
  • Teach AnsiTokenizer::Iterator::skip_ansi to correctly skip over arbitrary OSC sequences delimited by BEL or ST, without breaking existing CSI handling.
src/global/AnsiTextUtils.cpp
Propagate hyperlink information into text rendering, enabling clickable anchors and per-id hover underlining in the display widget.
  • Extend RawAnsi with QString url/urlId fields, update equality and helper constructors, and remove constexpr from RawAnsi methods and getRawAnsi helpers that now depend on QString.
  • Update updateFormat to set QTextCharFormat anchor/anchorHref based on RawAnsi.url and store RawAnsi.urlId into a custom QTextCharFormat property defined as AnsiTextHelper::URL_ID_PROPERTY.
  • In AnsiTextHelper::displayText, rely on parseAnsiColor for both SGR and OSC processing (removing the separate isAnsiColor gate) so that OSC 8 URL state is applied along with color changes.
src/global/AnsiTextUtils.h
src/global/AnsiTextUtils.cpp
src/client/displaywidget.h
src/client/displaywidget.cpp
Implement hyperlink activation semantics (including Mudlet send:/prompt: schemes and secure file:// handling) and synchronized hover underlining across fragments sharing an id.
  • Install an event filter and enable mouse tracking on the display widget viewport; implement DisplayWidget::eventFilter to detect mouse move/leave, read URL_ID_PROPERTY from the fragment under the cursor, and call updateHoverUnderline.
  • Implement DisplayWidget::updateHoverUnderline to recompute QTextEdit::ExtraSelection entries for all fragments in the document whose URL_ID_PROPERTY matches the hovered id, toggling single underline style and clearing selections on leave.
  • Connect anchorClicked to a lambda that dispatches by URL scheme: send: sends immediate input via DisplayWidgetOutputs, prompt: pre-fills the input widget, file: only opens local or localhost paths via QDesktopServices (logging and ignoring remote hosts), and otherwise opens URLs normally.
  • Add URL_ID_PROPERTY constant to AnsiTextHelper so both formatting and the event filter share the same property key, and store last hovered url id in DisplayWidget to avoid redundant recomputation.
src/client/displaywidget.cpp
src/client/displaywidget.h
Plumb new display-output methods to allow OSC 8 send:/prompt: handling to affect the input/telnet pipeline.
  • Extend DisplayWidgetOutputs with sendUserInput and setPrompt façade methods plus corresponding pure virtual hooks.
  • Implement virt_sendUserInput and virt_setPrompt in ClientWidget::initDisplayWidget’s LocalDisplayWidgetOutputs to call Telnet::sendToMud and StackedInputWidget::setPrompt respectively.
  • Add StackedInputWidget::setPrompt to write text into the current input widget, move the cursor to the end, and focus the widget.
src/client/displaywidget.h
src/client/ClientWidget.cpp
src/client/stackedinputwidget.h
src/client/stackedinputwidget.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The updateHoverUnderline implementation scans every block and fragment on every mouse move, which may become expensive for large scrollback buffers; consider caching fragments by urlId or limiting the scan to visible blocks only.
  • Using setExtraSelections to drive hover underlining will overwrite any existing extra selections (e.g., search highlights or other decorations); consider merging with, or layering on top of, other selection sources instead of replacing them outright.
  • The OSC-aware ANSI regex is now duplicated in both weakAnsiRegex and AnsiTextHelper::ansi_regex; it may be worth centralizing this pattern (or at least a shared helper) to avoid divergence if the OSC/CSI syntax needs adjustment later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `updateHoverUnderline` implementation scans every block and fragment on every mouse move, which may become expensive for large scrollback buffers; consider caching fragments by `urlId` or limiting the scan to visible blocks only.
- Using `setExtraSelections` to drive hover underlining will overwrite any existing extra selections (e.g., search highlights or other decorations); consider merging with, or layering on top of, other selection sources instead of replacing them outright.
- The OSC-aware ANSI regex is now duplicated in both `weakAnsiRegex` and `AnsiTextHelper::ansi_regex`; it may be worth centralizing this pattern (or at least a shared helper) to avoid divergence if the OSC/CSI syntax needs adjustment later.

## Individual Comments

### Comment 1
<location path="src/client/displaywidget.cpp" line_range="121-139" />
<code_context>
+    viewport()->installEventFilter(this);
+    viewport()->setMouseTracking(true);
+
+    connect(this, &DisplayWidget::anchorClicked, this, [this](const QUrl &url) {
+        QString scheme = url.scheme();
+        if (scheme == u"send") {
+            getOutput().sendUserInput(url.path() + u"\n");
+        } else if (scheme == u"prompt") {
+            // Pre-fill input widget
+            getOutput().setPrompt(url.path());
+        } else if (scheme == u"file") {
+            // Security check for file:// URIs
+            QString host = url.host();
+            if (host.isEmpty() || host == u"localhost" || host == QHostInfo::localHostName()) {
+                QDesktopServices::openUrl(url);
+            } else {
+                qWarning() << "OSC 8: Ignored file URI with non-local host:" << host;
+            }
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Consider tightening which URL schemes are opened via QDesktopServices to reduce potential misuse of OSC 8 links.

All other schemes are currently passed directly to QDesktopServices::openUrl, including arbitrary or custom ones coming from OSC 8 sequences. To reduce the impact of a malicious or misconfigured server, consider explicitly whitelisting allowed schemes (e.g. http, https, mailto) or blacklisting clearly unsafe ones before opening them.

```suggestion
    connect(this, &DisplayWidget::anchorClicked, this, [this](const QUrl &url) {
        QString scheme = url.scheme().toLower();
        if (scheme == u"send") {
            getOutput().sendUserInput(url.path() + u"\n");
        } else if (scheme == u"prompt") {
            // Pre-fill input widget
            getOutput().setPrompt(url.path());
        } else if (scheme == u"file") {
            // Security check for file:// URIs
            QString host = url.host();
            if (host.isEmpty() || host == u"localhost" || host == QHostInfo::localHostName()) {
                QDesktopServices::openUrl(url);
            } else {
                qWarning() << "OSC 8: Ignored file URI with non-local host:" << host;
            }
        } else if (scheme == u"http" || scheme == u"https" || scheme == u"mailto") {
            QDesktopServices::openUrl(url);
        } else {
            qWarning() << "OSC 8: Ignored URL with unsupported scheme:" << scheme << "url:" << url;
        }
    });
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +121 to +139
connect(this, &DisplayWidget::anchorClicked, this, [this](const QUrl &url) {
QString scheme = url.scheme();
if (scheme == u"send") {
getOutput().sendUserInput(url.path() + u"\n");
} else if (scheme == u"prompt") {
// Pre-fill input widget
getOutput().setPrompt(url.path());
} else if (scheme == u"file") {
// Security check for file:// URIs
QString host = url.host();
if (host.isEmpty() || host == u"localhost" || host == QHostInfo::localHostName()) {
QDesktopServices::openUrl(url);
} else {
qWarning() << "OSC 8: Ignored file URI with non-local host:" << host;
}
} else {
QDesktopServices::openUrl(url);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Consider tightening which URL schemes are opened via QDesktopServices to reduce potential misuse of OSC 8 links.

All other schemes are currently passed directly to QDesktopServices::openUrl, including arbitrary or custom ones coming from OSC 8 sequences. To reduce the impact of a malicious or misconfigured server, consider explicitly whitelisting allowed schemes (e.g. http, https, mailto) or blacklisting clearly unsafe ones before opening them.

Suggested change
connect(this, &DisplayWidget::anchorClicked, this, [this](const QUrl &url) {
QString scheme = url.scheme();
if (scheme == u"send") {
getOutput().sendUserInput(url.path() + u"\n");
} else if (scheme == u"prompt") {
// Pre-fill input widget
getOutput().setPrompt(url.path());
} else if (scheme == u"file") {
// Security check for file:// URIs
QString host = url.host();
if (host.isEmpty() || host == u"localhost" || host == QHostInfo::localHostName()) {
QDesktopServices::openUrl(url);
} else {
qWarning() << "OSC 8: Ignored file URI with non-local host:" << host;
}
} else {
QDesktopServices::openUrl(url);
}
});
connect(this, &DisplayWidget::anchorClicked, this, [this](const QUrl &url) {
QString scheme = url.scheme().toLower();
if (scheme == u"send") {
getOutput().sendUserInput(url.path() + u"\n");
} else if (scheme == u"prompt") {
// Pre-fill input widget
getOutput().setPrompt(url.path());
} else if (scheme == u"file") {
// Security check for file:// URIs
QString host = url.host();
if (host.isEmpty() || host == u"localhost" || host == QHostInfo::localHostName()) {
QDesktopServices::openUrl(url);
} else {
qWarning() << "OSC 8: Ignored file URI with non-local host:" << host;
}
} else if (scheme == u"http" || scheme == u"https" || scheme == u"mailto") {
QDesktopServices::openUrl(url);
} else {
qWarning() << "OSC 8: Ignored URL with unsupported scheme:" << scheme << "url:" << url;
}
});

@codecov

codecov Bot commented Apr 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.33333% with 94 lines in your changes missing coverage. Please review.
✅ Project coverage is 25.43%. Comparing base (a6c8653) to head (ba5fefa).

Files with missing lines Patch % Lines
src/client/displaywidget.cpp 0.00% 57 Missing ⚠️
src/global/AnsiTextUtils.cpp 70.58% 15 Missing ⚠️
src/client/stackedinputwidget.cpp 0.00% 4 Missing ⚠️
src/client/ClientWidget.cpp 0.00% 3 Missing ⚠️
src/map/World.cpp 0.00% 3 Missing ⚠️
src/client/displaywidget.h 0.00% 2 Missing ⚠️
src/map/ChangePrinter.cpp 0.00% 2 Missing ⚠️
src/map/ParseTree.cpp 0.00% 2 Missing ⚠️
src/mapdata/roomfilter.h 0.00% 2 Missing ⚠️
src/map/Remapping.cpp 0.00% 1 Missing ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #207      +/-   ##
==========================================
+ Coverage   25.40%   25.43%   +0.02%     
==========================================
  Files         519      519              
  Lines       43102    43228     +126     
  Branches     4698     4720      +22     
==========================================
+ Hits        10952    10995      +43     
- Misses      32150    32233      +83     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This commit implements support for terminal hyperlinks (OSC 8) in the
MMapper client. Key changes include:

- Extended `RawAnsi` struct to store `QString url` and `QString urlId`.
- Removed `constexpr` from `RawAnsi` and related formatting constants
  across the codebase (e.g., `Map.cpp`, `World.cpp`, `ChangePrinter.cpp`)
  as `RawAnsi` is no longer a literal type.
- Updated `AnsiTextUtils` to parse OSC 8 sequences with both ST and BEL
  terminators, including support for the optional `id` parameter.
- Enhanced `DisplayWidget` to render links as clickable anchors and
  implemented handling for `send:`, `prompt:`, and local `file:` schemes.
- Implemented synchronized underlining in `DisplayWidget` using a viewport
  event filter and extra selections to highlight all matching URL IDs
  on hover.
- Added `setPrompt` to `StackedInputWidget` and connected signals in
  `ClientWidget` to allow display interaction with the input buffer.
- Added unit tests for OSC 8 parsing in `TestGlobal`.
This commit implements support for terminal hyperlinks (OSC 8) in the
MMapper client, including fixes for clang-format violations identified in CI.

Key changes include:
- Extended `RawAnsi` struct to store `QString url` and `QString urlId`.
- Removed `constexpr` from `RawAnsi` and related formatting constants
  across the codebase (e.g., `Map.cpp`, `World.cpp`, `ChangePrinter.cpp`)
  as `RawAnsi` is no longer a literal type.
- Updated `AnsiTextUtils` to parse OSC 8 sequences with both ST and BEL
  terminators, including support for the optional `id` parameter.
- Enhanced `DisplayWidget` to render links as clickable anchors and
  implemented handling for `send:`, `prompt:`, and local `file:` schemes.
- Implemented synchronized underlining in `DisplayWidget` using a viewport
  event filter and extra selections to highlight all matching URL IDs
  on hover.
- Added `setPrompt` to `StackedInputWidget` and connected signals in
  `ClientWidget` to allow display interaction with the input buffer.
- Added unit tests for OSC 8 parsing in `TestGlobal`.
This commit implements support for terminal hyperlinks (OSC 8) in the
MMapper client, including fixes for build errors and deprecation
warnings identified in CI.

Key changes include:
- Extended `RawAnsi` struct to store `QString url` and `QString urlId`.
- Removed `constexpr` from `RawAnsi` and related constants as `RawAnsi`
  is no longer a literal type.
- Updated `AnsiTextUtils` to parse OSC 8 sequences with both ST and BEL
  terminators, and fixed a precision loss error (`shorten-64-to-32`).
- Fixed `QRegularExpression::match` and `globalMatch` deprecation
  warnings by using `matchView` and `globalMatchView` for Qt >= 6.6.
- Enhanced `DisplayWidget` to render links as clickable anchors and
  implemented interaction schemes (`send:`, `prompt:`, `file:`).
- Implemented synchronized underlining in `DisplayWidget` using a viewport
  event filter and extra selections.
- Added unit tests for OSC 8 parsing in `TestGlobal`.
- Ensured all modified files follow `clang-format` rules.
This commit implements support for terminal hyperlinks (OSC 8) in the
MMapper client, while resolving several issues identified in CI:

1.  **Precision Loss Fix**: Changed `int` to `qsizetype` for string
    indices in `AnsiTextUtils.cpp` to resolve `-Wshorten-64-to-32`.
2.  **Qt 6.8 Deprecation Fixes**: Wrapped `QRegularExpression::match` and
    `globalMatch` calls with version-guarded `matchView` and
    `globalMatchView` for Qt versions 6.6 and newer.
3.  **CI Dependency Fix**: Updated AppImage and Test workflows to use
    `libqt6svg6-dev` instead of the non-existent `qt6-svg-dev` package.
4.  **Code Quality**: Ensured all modified files are compliant with
    `clang-format`.

Core feature implementation:
- Extended `RawAnsi` to store URL data.
- Updated ANSI parser to recognize OSC 8 sequences.
- Enhanced `DisplayWidget` to render anchors and handle `send:`,
  `prompt:`, and local `file:` schemes.
- Implemented synchronized underlining for matching URL IDs on hover.
- Added unit tests for OSC 8 parsing.
This commit implements support for terminal hyperlinks (OSC 8) in the
MMapper client, while resolving several issues identified in CI:

1.  **Precision Loss Fix**: Changed `int` to `qsizetype` for string
    indices in `AnsiTextUtils.cpp` to resolve `-Wshorten-64-to-32`.
2.  **Qt 6.8 Deprecation Fixes**: Wrapped `QRegularExpression::match` and
    `globalMatch` calls with version-guarded `matchView` and
    `globalMatchView` for Qt versions 6.6 and newer.
3.  **CI Dependency Fix**: Updated AppImage and Test workflows to use
    `libqt6svg6-dev` instead of the non-existent `qt6-svg-dev` package.
4.  **Code Quality**: Ensured all modified files are compliant with
    `clang-format`.

Core feature implementation:
- Extended `RawAnsi` to store URL data.
- Updated ANSI parser to recognize OSC 8 sequences.
- Enhanced `DisplayWidget` to render anchors and handle `send:`,
  `prompt:`, and local `file:` schemes.
- Implemented synchronized underlining for matching URL IDs on hover.
- Added unit tests for OSC 8 parsing in `TestGlobal`.
This commit implements support for terminal hyperlinks (OSC 8) in the
MMapper client, while resolving several issues identified in CI:

1.  **Precision Loss Fix**: Changed `int` to `qsizetype` for string
    indices in `AnsiTextUtils.cpp` to resolve `-Wshorten-64-to-32`.
2.  **Qt 6.8 Deprecation Fixes**: Wrapped `QRegularExpression::match` and
    `globalMatch` calls with version-guarded `matchView` and
    `globalMatchView` for Qt versions 6.6 and newer.
3.  **QtGlobal Inclusion**: Added `#include <QtGlobal>` to ensure
    `QT_VERSION` is available for version guards.
4.  **CI Dependency Fix**: Updated AppImage, Test, and Release workflows
    to use `libqt6svg6-dev` instead of the non-existent `qt6-svg-dev`.
5.  **CMake Cleanup**: Changed `add_definitions` to
    `add_compile_definitions` for `QT_DISABLE_DEPRECATED_UP_TO`.
6.  **Code Quality**: Ensured all modified files are compliant with
    `clang-format`.

Core feature implementation:
- Extended `RawAnsi` to store URL data.
- Updated ANSI parser to recognize OSC 8 sequences.
- Enhanced `DisplayWidget` to render anchors and handle `send:`,
  `prompt:`, and local `file:` schemes.
- Implemented synchronized underlining for matching URL IDs on hover.
- Added unit tests for OSC 8 parsing.
This commit implements support for terminal hyperlinks (OSC 8) in the
MMapper client, while resolving several issues identified in CI:

1.  **Precision Loss Fix**: Changed `int` to `qsizetype` for string
    indices in `AnsiTextUtils.cpp` to resolve `-Wshorten-64-to-32`.
2.  **Qt 6.8 Deprecation Fixes**: Wrapped `QRegularExpression::match` and
    `globalMatch` calls with version-guarded `matchView` and
    `globalMatchView` for Qt versions 6.6 and newer.
3.  **QtGlobal Inclusion**: Added `#include <QtGlobal>` to ensure
    `QT_VERSION` is available for version guards.
4.  **Safety Fix**: Fixed unsafe `matchView` usage in `UpdateDialog.cpp`
    by ensuring the subject `QString` outlives the match object.
5.  **CMake Cleanup**: Changed `add_definitions` to
    `add_compile_definitions` for `QT_DISABLE_DEPRECATED_UP_TO`.
6.  **Code Quality**: Ensured all modified files are compliant with
    `clang-format`.

Core feature implementation:
- Extended `RawAnsi` to store URL data.
- Updated ANSI parser to recognize OSC 8 sequences.
- Enhanced `DisplayWidget` to render anchors and handle `send:`,
  `prompt:`, and local `file:` schemes.
- Implemented synchronized underlining for matching URL IDs on hover.
- Added unit tests for OSC 8 parsing.
@nschimme
nschimme force-pushed the master branch 2 times, most recently from e8139f3 to c119262 Compare April 20, 2026 18:27
@nschimme
nschimme force-pushed the master branch 3 times, most recently from ae664f2 to bcd8fca Compare May 21, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant